﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data.Common;
using System.IO;
using System.Linq;
using System.Threading;
using Dataglot;
using IrishSettings.Installation;
using ProtoBuf;
using Wrappers.WebContext;

namespace IrishSettings
{

    /// <summary>
    /// This is the root object, the main API for other apps to use the IrishSettings library
    /// </summary>
    public class IrishSettingsManager : IIrishSettingsManager
    {
        private SettingsSchema schema;
        private SettingsApplication application;
        private SettingsValues values;
        private Dictionary<string, SettingsValues> perUserValues;

        private bool isInitialized;

        private readonly HttpRequestWrapper request;

        public IrishSettingsManager(SettingsApplication newApplication)
        {
            application = newApplication;
            Construct(newApplication.Schema);
        }


        public IEnumerable<GenericSetting> Enumeration()
        {
            return values.Values;
        }

        int retries = 0;

        private void Construct(SettingsSchema newSchema)
        {
            if (newSchema==null)
                throw new InvalidOperationException("newSchema can't be null or blank");
    
            DataFactory factory = new DataFactory(
                            ConfigurationManager.AppSettings["Dataglot.ConnectionStringName"]
                        );

            //If already saved, ignore constructor schema.
            DbApplicationManager appManager = new DbApplicationManager(factory);

            try
            {
                //Create or load
                SettingsApplication currentApp = appManager.CreateApplication(factory, application,
                                                                              new DbSchemaManager(application, factory));
                DbSchemaManager schemaManager = new DbSchemaManager(currentApp, factory);
                if (schemaManager.SchemaIsStored(application, application.VersionObject))
                {
                    LoadForCurrent(newSchema, schemaManager);
                }
                else
                {
                    schemaManager.InsertSchema(newSchema);
                    LoadForCurrent(newSchema, schemaManager);
                    //throw new InvalidOperationException(
                        //"Schema needs to be saved to database before attempting to use it in a Settings Manager");
                }
            }
            catch(DbException ex)
            {
                if(ex.Message.Contains("Invalid object name") && retries==0)
                {
                    InstallIrishSettings.CreateAllTables(ConfigurationManager.AppSettings["Dataglot.ConnectionStringName"],
                        ConfigurationManager.AppSettings["Dataglot.Namespace"]);
                    retries++;
                    Construct(newSchema);
                }
                else
                    throw;
            }
        }

        private void LoadForCurrent(SettingsSchema newSchema, DbSchemaManager schemaManager)
        {
            schema = schemaManager.LoadSchemaForCurrentEnvironment(application.VersionObject);
            newSchema.Validate();

            foreach (var item in newSchema)
            {
                if (item.Value.Id == 0)
                {
                    throw new InvalidOperationException("This schema doesn't have Id's");
                }
            }
            schema = newSchema;
        }

        public IrishSettingsManager(SettingsSchema newSchema, HttpRequestWrapper newRequest)
        {
            request = newRequest;

            Construct(newSchema);
        }

        public SettingsValues Initialize()
        {
            if (isInitialized) 
                return values;

            //Load settings
            LoadValues();
            //Add missing
            AddMissingValuesUsingDefaults();


            application.ValidateRequirements();

            foreach (GenericSettingSchema settingsValue in schema.Values)
            {
                settingsValue.ValidateRequirements();
            }

            
            isInitialized = true;
            return values;
        }

        private void AddMissingValuesUsingDefaults()
        {
            foreach(var schemaItem in schema)
            {
                if(!values.ContainsKey(schemaItem.Key))
                {
                    //Add it
                    GenericSetting missingSetting = new GenericSetting(schemaItem.Value, application);
                    missingSetting.SchemaId = schemaItem.Value.Id;
                    missingSetting.Value = schemaItem.Value.DefaultSerializedValue;
                    missingSetting.SerializedValue = missingSetting.Value.ToString();
                    missingSetting.Version = application.Version;
                    
                    values.ValidateAndAdd(missingSetting.Schema.Name, missingSetting);
                }
            }
        }


        private void LoadValues()
        {
            //Is schema set by code?  
            //TODO: This isn't the best way to detect if the schema is from code (or loaded from db)
            if(schema.Count>0)
            {
                //Has this already been done? If we have PK's then yes.
                //Validate if we have PKs and FKs

                //We have a schema, does it match the db?
                //TODO: compare schemas?
            }

            foreach (GenericSettingSchema schemaRow in schema.Values)
            {
                schemaRow.ValidateRequirements();

            }

            values = new SettingsValues();

            //Load Application settings values from DB
            using (SelectAllApplicationSettings select = new SelectAllApplicationSettings(ConfigUtils.DefaultDataFactory()))
            {
                using (DbDataReader reader = select.Select(application.Application, application.VersionObject))
                {
                    while (reader.Read())
                    {
                        GenericSetting setting = new GenericSetting(schema[reader.GetString(reader.GetOrdinal("name"))], application);
                        setting.SchemaId = setting.Schema.Id; 
                        setting.SerializedValue = reader.GetString(reader.GetOrdinal("serialized_value"));
                        
                        setting.Value = setting.SerializedValue;
                        setting.Version = reader.GetString(reader.GetOrdinal("version"));
                        setting.ValidateRequirements();
                        values.Add(setting.Schema.Name, setting);
                    }
                }
            }

            foreach (KeyValuePair<string, GenericSetting> settingsValue in values)
            {
                if (settingsValue.Value.Schema.Id != settingsValue.Value.SchemaId)
                {
                    throw new InvalidOperationException("The id's are out of synch.");
                }
            }

            foreach (GenericSetting row in values.Values)
            {
                row.ValidateRequirements();
                row.Schema.ValidateRequirements();
            }

            ////Not in DB? Load defaults.
            //foreach (var schemaItem in schema)
            //{
            //    if (!values.ContainsKey(schemaItem.Key))
            //    {
            //        GenericSetting setting = new GenericSetting(schemaItem.Value);
            //        setting.SchemaId = schemaItem.Value.Id;
            //        setting.Version = schemaItem.Value.Application.Version;
            //        setting.Value = schemaItem.Value.DefaultSerializedValue;
                    
            //        values.Add(setting.Schema.Name, setting);
            //    }
            //}

            foreach (GenericSetting settingsValue in values.Values)
            {
                settingsValue.SchemaId = settingsValue.Schema.Id;
            }

            //Are values valid?
            foreach (GenericSetting settingsValue in values.Values)
            {
                settingsValue.ValidateRequirements();
            }

            LoadPerUserSettingsValues();

            isInitialized = true;
            //return values;   
        }

        private void LoadPerUserSettingsValues()
        {
            bool hasByUser = (from item in schema
                              where item.Value.IsUser == true
                              select 1).Sum() > 0;

            bool hasByAnonymousSettings = (from item in schema
                                           where item.Value.IsUser == true
                                                 && item.Value.IsAnonymous == true 
                                           select 1).Sum() > 0;


            string anonymousName =null;
            if (request != null)
            {
                if (hasByAnonymousSettings && !Thread.CurrentPrincipal.Identity.IsAuthenticated)
                {
                    ProcessAnonymousUsers process = new ProcessAnonymousUsers(request);
                    anonymousName = process.LogonAnonymous();
                }
            }

            //Only need user settings if authenticated
            //Else, we need only check for anonymous.
            if ((hasByUser && Thread.CurrentPrincipal.Identity.IsAuthenticated) 
                || (hasByAnonymousSettings && !string.IsNullOrEmpty(anonymousName)))
            {

                //Load User stuff
                if(perUserValues==null)
                {
                    perUserValues=new Dictionary<string, SettingsValues>();
                }


                //TODO: Load values.
            }
        }

        

        public void Set(string name, object value)
        {
            MemoryStream stream = new MemoryStream();
            Serializer.Serialize(stream, value);

            string serializedValue = Convert.ToBase64String(stream.GetBuffer());

            //Mark to save to DB
        }

        /// <summary>
        /// Saves SettingsDictionary values to the database
        /// </summary>
        public void Save()
        {
            if (!isInitialized) Initialize();

            //Reserialize all values.
            SerializeValues();

            //Save all to db.

            DataFactory factory = new DataFactory(ConfigUtils.OleDbConnectionString(), ConfigUtils.Provider());
            using (DbConnection con = factory.Connection())
            {
                con.Open();
                using (TransactionManager manager = new TransactionManager(con))
                using (SettingsForApplicationController controller = new SettingsForApplicationController(factory,con, manager.Transaction))
                {
                    foreach (KeyValuePair<string, GenericSetting> settingsValue in values)
                    {
                        if(settingsValue.Value.Id>0)
                        {
                            controller.Update(settingsValue.Value);
                        }
                        else if (controller.ExistsByName(settingsValue.Key, application.Application, application.VersionObject))
                        {
                            //OK, the row is there, we just didn't have the primary key
                            GenericSettingStored outputValue = controller.SelectByName(settingsValue.Key, application.Application,
                                                    application.VersionObject);
                            settingsValue.Value.Id = outputValue.Id;
                            controller.Update(settingsValue.Value);
                        }
                        else
                        {
                            //Nothing there.
                            GenericSettingStored returnValue = controller.Insert(settingsValue.Value);
                            settingsValue.Value.Id = returnValue.Id;
                        }
                        
                    }

                    manager.Commit();
                }
            }
        }

        public void SerializeValues()
        {
            foreach (KeyValuePair<string, GenericSetting> settingsValue in values)
            {
                string type = schema[settingsValue.Key].ValueType;
                if( type=="System.String")
                {
                    settingsValue.Value.SerializedValue = settingsValue.Value.Value.ToString();
                }
                else if(type=="System.Int32")
                {
                    settingsValue.Value.SerializedValue = settingsValue.Value.Value.ToString(); 
                    //((Int32)settingsValue.Value.Value).ToString(schema.Culture.NumberFormat);
                }
                else if (type == "System.DateTime")
                {
                    settingsValue.Value.SerializedValue = (Convert.ToDateTime(settingsValue.Value.Value)).ToString(application.CultureObject.DateTimeFormat);
                }
                else
                {
                    //TODO: Micro-optimization opportunity, update only the changed ones.
                    MemoryStream stream = new MemoryStream();
                    Serializer.Serialize(stream, settingsValue.Value.Value);

                    settingsValue.Value.SerializedValue = Convert.ToBase64String(stream.GetBuffer());
                }

            }
        }

        public GenericSetting Setting(string name)
        {
            if(!isInitialized) 
                Initialize();

            if(values.ContainsKey(name))
            {
                return values[name];
            }
            //else
            LoadValues();
            if (values.ContainsKey(name))
            {
                return values[name];
            }
            //else
            throw new InvalidOperationException("Can't find key, even tried reloading from database");
        }

        public GenericSetting this[string name]
        {
            get
            {
                return Setting(name);
            }
            set
            {
                throw new NotImplementedException();
                //base[name] = value;
            }
        }

        /// <summary>
        /// Same as Load Settings, but 1 version back.
        /// </summary>
        /// <returns></returns>
        public Version VersionPrevious()
        {
            using (SelectApplicationVersions command = new SelectApplicationVersions(ConfigUtils.DefaultDataFactory()))
            {
                List<Version> versions = command.Select(application.Application);
                Version currentVersion = versions.Max();

                Version lastVersion = (from v in versions where v < currentVersion select v).Max();

                return lastVersion;
            }
        }

        public Version VersionCurrent()
        {
            using (SelectApplicationVersions command = new SelectApplicationVersions(ConfigUtils.DefaultDataFactory()))
            {
                List<Version> versions = command.Select(application.Application);
                Version currentVersion = versions.Max();
                return currentVersion;
            }
        }

        /// <summary>
        /// Put all values back to defaults.
        /// </summary>
        public void Reset()
        {
            using (ResetSettings command = new ResetSettings(ConfigUtils.DefaultDataFactory()))
            {
                command.ResetAllVersions(application.Application);
            }
        }
    }

   
}
